// Author	: damphat@gmail.com
// Created	: 01/12/2008

#pragma once
#include "vn/NetCommon.h"
#include "vn/SocketException.h"
#include "vn/EndPoint.h"

#include "vn/IPAddress.h"
#include "vn/IPEndPoint.h"
#include "vn/Dns.h"


#define SOCKET_GET_OPT(PropType, PropName, OPTLEVEL, OPTNAME, OPTTYPE)		\
PropType Get##PropName() {													\
	OPTTYPE val;															\
	int	len = sizeof(OPTTYPE);												\
	int ret = getsockopt(m_sock, OPTLEVEL, OPTNAME, (LPChar)&val, &len);	\
	CHECK_WINSOCK(ret != SOCKET_ERROR);										\
	assert(len == sizeof(OPTTYPE));											\
	return PropType(val);													\
}

#define SOCKET_SET_OPT(PropType, PropName, OPTLEVEL, OPTNAME, OPTTYPE)		\
void Set##PropName(PropType inp){											\
	OPTTYPE val = OPTTYPE(inp);												\
	int ret = setsockopt(													\
		m_sock, OPTLEVEL, OPTNAME,											\
		(LPChar)&val, sizeof(OPTTYPE));										\
	CHECK_WINSOCK(ret != SOCKET_ERROR);										\
}


#define SOCKET_GETSET_OPT(PropType, PropName, OPTLEVEL, OPTNAME, OPTTYPE)	\
	SOCKET_GET_OPT(PropType, PropName, OPTLEVEL, OPTNAME, OPTTYPE)			\
	SOCKET_SET_OPT(PropType, PropName, OPTLEVEL, OPTNAME, OPTTYPE)


NET_BEGIN

enum SocketStatus {
	StatusClose			= (0 << 0),
	StatusOpen			= (1 << 0),
	StatusConnected		= (1 << 1) + StatusOpen,
	StatusBound			= (1 << 2) + StatusOpen,
	StatusListening		= (1 << 3) + StatusOpen + StatusBound,
};

enum SelectMode {
	SelectRead,
	SelectWrite,
	SelectError
};

enum SocketShutdown {
	ShutdownBoth	= SD_BOTH,
	ShutdownReceive	= SD_RECEIVE,
	ShutdownSend	= SD_SEND
};

class Socket {
private:
	Socket(Socket&);
	void operator =(Socket&);
public:
	Socket() : m_sock(0), m_Status(StatusClose), m_willBlock(true)
	{
		SocketStartup();	// MAY THROW?
	}

	void Open(int addressFamily, int type, int protocol) {
		CHECK_STATUS(m_Status == StatusClose);
		SOCKET sret = socket(addressFamily, type, protocol);
		CHECK_WINSOCK(sret != INVALID_SOCKET);
		m_sock = sret;
		m_Status = StatusOpen;
		m_willBlock = true;
		m_addressFamily = addressFamily;
	}

	void OpenTcp() {
		CHECK_STATUS(m_Status == StatusClose);
		Open(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	}

	void OpenUdp() {
		CHECK_STATUS(m_Status == StatusClose);
		Open(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
	}

	//Establishes a connection to a remote host.
	void Connect(const EndPoint &ep) {
		CHECK_ARG(ep.GetAddressFamily() == m_addressFamily);
		CHECK_STATUS(m_Status == StatusOpen);
		// disallow connect on nonblocking because it is difficult to user
		CHECK_STATUS(m_willBlock);
		int ret = connect(m_sock, (const sockaddr *)ep.GetData(), ep.GetLength());
		// REVIEW, a studycase for designer:
		// You can try call Socket::Connect again if wsaError are WSAECONNREFUSED,
		// WSAENETUNREACH, WSAETIMEDOUT, for other error you cannot.
		CHECK_WINSOCK(ret != SOCKET_ERROR);
		m_Status = StatusConnected;
	}

	void Connect(const IPAddress &ip, int port) {
		CHECK_ARG(m_addressFamily == ip.GetAddressFamily());
		CHECK_ARG_RANGE_PORT(port);
		CHECK_STATUS(m_Status == StatusOpen);
		Connect(IPEndPoint(ip, port));
	}

	void Connect(String hostname, int port) {
		CHECK_ARG_NOT_NULL(hostname.C_STR());
		CHECK_ARG_RANGE_PORT(port);
		CHECK_STATUS(m_Status == StatusOpen);
		IPHostEntry ent = Dns::GetHostEntry(hostname);
		Connect(ent.AddressList[0], port);
	}

	//Associates a Socket with a local endpoint.
	//can rebind?
	void Bind(const EndPoint &ep) {
		CHECK_ARG(ep.GetAddressFamily() == m_addressFamily);
		CHECK_STATUS(m_Status == StatusOpen);
		int ret = bind(m_sock, (const sockaddr *)ep.GetData(), ep.GetLength());
		CHECK_WINSOCK(ret != SOCKET_ERROR);
		m_Status = StatusBound;
	}

	void Bind(const IPAddress &ip, int port) {
		CHECK_ARG(m_addressFamily == ip.GetAddressFamily());
		CHECK_ARG_RANGE_PORT(port);
		CHECK_STATUS(m_Status == StatusOpen);
		Bind(IPEndPoint(ip, port));
	}

	//Places a Socket in a listening state.
	void Listen(int backlog = SOMAXCONN) {
		CHECK_ARG(backlog > 0);
		CHECK_STATUS(m_Status == StatusBound);
		int ret = listen(m_sock, backlog);
		CHECK_WINSOCK(ret != SOCKET_ERROR);
		m_Status = StatusListening;
	}

	//Creates a new Socket for a newly created connection.
	void Accept(Socket &out) {
		CHECK_ARG(out.m_Status == StatusClose); // out must be a closed socket
		CHECK_STATUS(m_Status == StatusListening);
		SOCKET sret = accept(m_sock, 0, 0);		// TODO: get right_endpoint
		CHECK_WINSOCK(sret != INVALID_SOCKET);
		out.m_sock = sret;
		out.m_willBlock = true;
		out.m_addressFamily = m_addressFamily;
	}

	//Receives data from a bound Socket.
	int Recv(Buf8 *buf, int len, int flags = 0) {
		CHECK_ARG_BUFFER(buf, len);
		CHECK_ARG_FLAGS(flags, MSG_PEEK | MSG_OOB);
		CHECK_STATUS((m_Status == StatusConnected) || (m_Status == StatusBound)); // SURE?
		int ret = recv(m_sock, buf, len, flags);
		CHECK_WINSOCK(ret != SOCKET_ERROR);
		return ret;
	}

	//Receives a datagram and stores the source endpoint.
	//from is for output
	//from = null?
	//need some simple overloaded functions
	void RecvFrom(Buf8 *buf, int len, int flags, EndPoint &from){
		CHECK_ARG_BUFFER(buf, len);
		CHECK_ARG_FLAGS(flags, MSG_PEEK | MSG_OOB);
		CHECK_STATUS(m_Status == StatusBound);
		CHECK_ARG(m_addressFamily == from.GetAddressFamily());
		int fromlen = from.GetLength();
		int ret = recvfrom(m_sock, buf, len, flags, (sockaddr *)from.GetData(), &fromlen);
		CHECK_WINSOCK(ret != SOCKET_ERROR);
	}

	//Sends data to a connected Socket.
	//return value can be less than len(which case?)
	int Send(const Buf8 *buf, int len, int flags = 0) {
		CHECK_ARG_BUFFER(buf, len);
		CHECK_ARG_FLAGS(flags, MSG_DONTROUTE | MSG_OOB)
		CHECK_STATUS((m_Status == StatusConnected) || (m_Status == StatusBound));	// SURE?
		int ret = send(m_sock, buf, len, flags);
		CHECK_WINSOCK(ret != SOCKET_ERROR);
		return ret;	// which can be less than the number indicated by len
	}

	//Sends data to a specific endpoint.
	//need some simple overloaded functions
	void SendTo(const Buf8 *buf, int len, int flags, const EndPoint &to){
		CHECK_ARG_BUFFER(buf, len);
		CHECK_ARG_FLAGS(flags, MSG_DONTROUTE | MSG_OOB)
		CHECK_STATUS(m_Status == StatusBound);
		CHECK_ARG(m_addressFamily == to.GetAddressFamily());
		int ret = sendto(m_sock, buf, len, flags, (const sockaddr *)to.GetData(), to.GetLength());
		CHECK_WINSOCK(ret != SOCKET_ERROR);
	}

	//Disables sends and receives on a Socket.
	void Shutdown(SocketShutdown how) {
		CHECK_STATUS(m_Status == StatusConnected);	// connected socket only
		int ret = shutdown(m_sock, how);
		CHECK_WINSOCK(ret != SOCKET_ERROR);
		// changed status here?
	}

	//Closes the Socket connection and releases all associated resources.
	void Close() {
		if(m_sock) {
			int ret = closesocket(m_sock);
			CHECK_WINSOCK(ret != SOCKET_ERROR);
			m_sock = 0;
			m_Status = StatusClose;
		}
	}

	// Sets low-level operating modes for the Socket.
	void IOControl(long cmd, ULong *argp) {
		CHECK_ARG(cmd != FIONBIO);	// should use Socket::SetBlocking
		CHECK_STATUS(m_Status != StatusClose);
		int ret = ioctlsocket(m_sock, cmd, argp);
		CHECK_WINSOCK(ret != SOCKET_ERROR);
	}

	// GetSocketOption	Overloaded. Returns the value of a Socket option.
	void GetSocketOption(int level, int optname, Buf8 * optval, int* optlen) {
		CHECK_ARG_BUFFER(optval, optlen);
		CHECK_STATUS(m_Status != StatusClose);
		int ret = getsockopt(m_sock, level, optname, optval, optlen);
		CHECK_WINSOCK(ret != SOCKET_ERROR);
	}

	//Sets a Socket option.
	void SetSocketOption(int level, int optname, const Buf8 * optval, int optlen) {
		CHECK_ARG_BUFFER(optval, optlen);
		CHECK_STATUS(m_Status != StatusClose);
		int ret = setsockopt(m_sock, level, optname, optval, optlen);
		CHECK_WINSOCK(ret != SOCKET_ERROR);
	}

	//Determines the status of the Socket.
	bool Poll(int microSeconds, SelectMode mode) {
		fd_set set;
		FD_ZERO(&set);
		FD_SET(m_sock, &set);
		timeval tv;
		timeval *ptv;
		if(microSeconds == -1) {
			ptv = 0;
		}
		else {
			tv.tv_sec = microSeconds / 1000000;
			tv.tv_usec = microSeconds % 1000000;
			ptv = &tv;
		}

		int ret;
		switch(mode) {
		case SelectRead:
			ret = select(0, &set, 0, 0, ptv);
			break;
		case SelectWrite:
			ret = select(0, 0, &set, 0, ptv);
			break;
		case SelectError:
			ret = select(0, 0, 0, &set, ptv);
			break;
		}

		CHECK_WINSOCK(ret != SOCKET_ERROR);
		return FD_ISSET(m_sock, &set) != 0;
	}

	// static Select:	Determines the status of one or more sockets.
	template <class OPTTYPE>
	OPTTYPE GetOption(int level, int optname) {
		CHECK_STATUS(m_Status != StatusClose);
		OPTTYPE out;
		int		len = sizeof(OPTTYPE);
		GetSocketOption(level, optname, (Buf8 *)&out, &len);
		assert(len == sizeof(OPTTYPE));
		return out;
	}

	template <class OPTTYPE>
	void SetOption(int level, int optname, OPTTYPE inp) {
		CHECK_STATUS(m_Status != StatusClose);
		SetSocketOption(level, optname, (const Buf8 *)&inp, sizeof(OPTTYPE));
	}

	#pragma warning(disable : 4800)
	// Indicates that data should not be fragmented regardless of the local MTU. Valid only for message oriented protocols. All Microsoft providers silently ignore this option.
//	SOCKET_GETSET_OPT(bool,	DontFragment,		IPPROTO_IP,		IP_DONTFRAGMENT, DWORD);
	// Changes the default value set by the TCP/IP service provider in the TTL field of the IP header in outgoing datagrams. IP_TTL support is not required; to check whether IP_TTL is supported, use getsockopt to get current options. If getsockopt fails, IP_TTL is not supported.
//	SOCKET_GETSET_OPT(short,	Ttl,			IPPROTO_IP,		IP_TTL, DWORD);
	// Configure socket for sending broadcast data. Valid only for protocols that support broadcasting (IPX, UDP/IPv4, and others)
	SOCKET_GETSET_OPT(bool,	EnableBroadcast,	SOL_SOCKET,		SO_BROADCAST, DWORD);
	//Enables multicast loopback when set to TRUE. When TRUE, and the socket is also joined to the multicast destination group, multicast data sent on the socket is echoed to its receive buffer.
//	SOCKET_GETSET_OPT(bool,	MulticastLoopback,	IPPROTO_IP,		IP_MULTICAST_LOOP, DWORD);
	// Prevents any other socket from binding to the same address and port. Option must be set before bind. See Using SO_EXCLUSIVEADDRUSE for more information.
	SOCKET_GETSET_OPT(bool,	ExclusiveAddressUse,SOL_SOCKET,		SO_EXCLUSIVEADDRUSE, DWORD);
	//Associates a maximum time the system will wait before aborting a connection after socket has been closed. Valid for reliable, connection oriented protocols only.
	SOCKET_GETSET_OPT(LINGER,	LingerState,	SOL_SOCKET,		SO_LINGER, LINGER);
	//Enables or disables the Nagle algorithm for TCP sockets. Enabled by default.
	SOCKET_GETSET_OPT(bool,	NoDelay,			IPPROTO_TCP,	TCP_NODELAY, DWORD);
	//Sets a timeout (in milliseconds) for receive calls. If a receive call times out, the connection is in an undeterminate state and should be closed.
	SOCKET_GETSET_OPT(int,	ReceiveTimeout,		SOL_SOCKET,		SO_RCVTIMEO, DWORD);
	//Sets the per-socket buffer size for receiving data
	SOCKET_GETSET_OPT(int,	ReceiveBufferSize,	SOL_SOCKET,		SO_RCVBUF, DWORD);
	//Sets the per-socket buffer size for sending data
	SOCKET_GETSET_OPT(int,	SendBufferSize,		SOL_SOCKET,		SO_SNDBUF, DWORD);
	//Sets a timeout (in milliseconds) for send calls. If a send call times out, the connection is in an undeterminate state and should be closed.
	SOCKET_GETSET_OPT(int,	SendTimeout,		SOL_SOCKET,		SO_SNDTIMEO, DWORD);
	//Returns the socket type for the given socket (e.g. SOCK_STREAM, SOCK_DGRAM, etc.)
	SOCKET_GET_OPT	 (DWORD, SocketType,		SOL_SOCKET,		SO_TYPE, DWORD);
	#pragma warning(default : 4800)

	//bool IsBound { get; }
	//bool Connected { get; }
	//bool UseOnlyOverlappedIO { get; set; }

	//ProtocolType ProtocolType { get; }

	int GetAddressFamily() {
		return m_addressFamily;
	}

	void GetRemoteEndPoint(EndPoint &ep) {
		CHECK_STATUS(m_Status != StatusClose); // CHECK_STATUS before calling GetAddressFamily()
		CHECK_ARG(ep.GetAddressFamily() == m_addressFamily);
		int len = ep.GetLength();
		int ret = getpeername(m_sock, (sockaddr *)ep.GetData(), &len);
		assert(len != ep.GetLength()); // coding error
		CHECK_WINSOCK(ret != SOCKET_ERROR);
	}

	void GetLocalEndPoint(EndPoint &ep) {
		CHECK_STATUS(m_Status != StatusClose); // CHECK_STATUS before calling GetAddressFamily()
		CHECK_ARG(GetAddressFamily() == ep.GetAddressFamily());
		int len = ep.GetLength();
		int ret = getsockname(m_sock, (sockaddr *)ep.GetData(), &len);
		assert(len != ep.GetLength()); // coding error
		CHECK_WINSOCK(ret != SOCKET_ERROR);
	}

	// setter only
	void SetBlocking(bool b) {
		CHECK_STATUS(m_Status != StatusClose);
		ULong ul = b;
		IOControl(FIONBIO , &ul);	// has exception
		m_willBlock = b;
	}

	bool GetBlocking() {
		CHECK_STATUS(m_Status != StatusClose);
		return m_willBlock;
	}

	int GetAvailable() {
		CHECK_STATUS(m_Status != StatusClose);
		ULong ul;
		IOControl(FIONREAD , &ul);
		return ul;
	}

	SocketStatus GetStatus() {
		return m_Status;
	}

	virtual ~Socket() {
		try {
			Close();
		}
		catch(Exception&) {
			// FALTAL("Exception in destructor, send to lib developer!");
		}
	}

public:
	SOCKET			m_sock;
	SocketStatus	m_Status;
	bool			m_willBlock;
	int				m_addressFamily;
};

NET_END

// Status: Close/Open/Connected/Disconnected

// MFC Library Reference
// CSocket::CancelBlockingCall

// Try to use nonblocking for send-receive
